/*
 * Copyright (C) 2011 by Anton Novikau
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.googlecode.gplaces;

import com.googlecode.gplaces.core.AsyncTaskResult;
import com.googlecode.gplaces.core.DialogBasedAsyncTask;
import com.googlecode.gplaces.core.GplacesApp;
import com.googlecode.gplaces.core.LocationObtainer;
import com.googlecode.gplaces.core.QuotaLimitException;
import com.googlecode.gplaces.data.Place;
import com.googlecode.gplaces.parser.PlacesParser;

import org.json.JSONException;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Main activity used to query Google Places service for GIS objects that are
 * near by the current location. All founded places stored into a
 * {@link ListView}. To share particular item user can click on it and choose a
 * phone number from address book. When contact is picked up, standard SMS
 * messenger suggest user to send a short message with predefined text or edit
 * it before sending.
 * 
 * @author Anton Novikov
 */
public class GplacesActivity extends Activity implements View.OnClickListener,
        AdapterView.OnItemClickListener {

    /**
     * Content type used to resolve standard SMS messenger Application.
     */
    private static final String SMS_CONTENT_TYPE = "vnd.android-dir/mms-sms";

    /**
     * SMS body extra parameter used by default SMS messenger.
     */
    private static final String EXTRA_SMS_BODY = "sms_body";

    /**
     * Phone number extra parameter used by default SMS messenger.
     */
    private static final String EXTRA_SMS_ADDRESS = "address";

    private PlacesAdapter placesAdapter;

    private List<Place> placesList;

    private LocationManager locationManager;

    private int pickedPosition;

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.places_list);

        // resolve system services.
        locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

        // restore activity state
        StateHolder holder = (StateHolder) getLastNonConfigurationInstance();
        if (holder == null) {
            placesList = new ArrayList<Place>();
            pickedPosition = -1;
        } else {
            placesList = holder.placesList;
            pickedPosition = holder.pickedPosition;
        }

        // init views
        ListView placesListView = (ListView) findViewById(R.id.places_list);
        placesAdapter = new PlacesAdapter(placesList, getApplicationContext());
        placesListView.setAdapter(placesAdapter);
        placesListView.setOnItemClickListener(this);
        findViewById(R.id.find_near_by_btn).setOnClickListener(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onClick(View v) {
        if (R.id.find_near_by_btn == v.getId()) {
            findPlacesNearBy();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if (R.id.places_list == parent.getId()) {
            // cache current picked position to know what item would be used on
            // activity result.
            pickedPosition = position;
            // start phone number picker.
            Intent intent = new Intent(Intent.ACTION_PICK,
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI);
            startActivityForResult(intent, R.id.contact_selector_rq);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case R.id.contact_selector_rq:
                if (RESULT_OK == resultCode) {
                    // open sms messenger with picked contact as addressee.
                    sendSms(data.getData());
                }
                break;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // detaching current activity from application.
        ((GplacesApp) getApplication()).detach(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        // attaching restored activity to application.
        ((GplacesApp) getApplication()).attach(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public StateHolder onRetainNonConfigurationInstance() {
        // store statefull data into holder on orientation or other
        // configuration change.
        StateHolder holder = new StateHolder();
        holder.placesList = this.placesList;
        holder.pickedPosition = this.pickedPosition;
        return holder;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.clear_search_item:
                placesList.clear();
                placesAdapter.notifyDataSetChanged();
                return true;
            case R.id.settings_item:
                startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected Dialog onCreateDialog(int id) {
        Dialog dialog;
        switch (id) {
            case R.id.finding_progress_dialog:
                dialog = createProgressDialog(R.string.finding_progress_msg);
                break;
            case R.id.obtaining_location_dialog:
                dialog = createProgressDialog(R.string.obtaining_location_msg);
                break;
            case R.id.no_results_dialog:
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setTitle(R.string.google_places_title);
                builder.setMessage(R.string.no_results_msg);
                builder.setCancelable(false);
                // special key listener that protects from dismissing dialog on
                // Search button press.
                builder.setOnKeyListener(new DismissableKeyListener());
                builder.setPositiveButton(R.string.yesBtn, new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        findPlacesNearBy();
                    }

                });
                builder.setNegativeButton(R.string.noBtn, new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {}

                });
                dialog = builder.create();
                break;
            case R.id.network_error_code:
                dialog = createErrorDialog(R.string.network_error_msg);
                break;
            case R.id.parser_error_code:
                dialog = createErrorDialog(R.string.parser_error_msg);
                break;
            case R.id.quota_limit_error_code:
                dialog = createErrorDialog(R.string.quota_limit_msg);
                break;
            case R.id.unknown_error_code:
                dialog = createErrorDialog(R.string.unknown_error_msg);
                break;
            default:
                dialog = super.onCreateDialog(id);
                break;
        }
        return dialog;
    }

    // method to create error dialog with specified message.
    private Dialog createErrorDialog(int msgId) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.error_title);
        builder.setMessage(msgId);
        builder.setCancelable(false);
        // special key listener that protects from dismissing dialog on Search
        // button press.
        builder.setOnKeyListener(new DismissableKeyListener());
        builder.setPositiveButton(R.string.okBtn, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {}
        });
        return builder.create();
    }

    // method created indeterminate progress dialog with specified message.
    private ProgressDialog createProgressDialog(int msgId) {
        ProgressDialog dialog = new ProgressDialog(this);
        dialog.setMessage(getString(msgId));
        dialog.setIndeterminate(true);
        dialog.setCancelable(false);

        // progress dialogs are not cancelable.
        dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {

            @Override
            public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                if (KeyEvent.KEYCODE_SEARCH == keyCode) {
                    return true;
                }
                return false;
            }

        });
        return dialog;
    }

    // method performs search of places near by current location.
    private void findPlacesNearBy() {
        // obtain current location.
        LocationObtainer obtainer = new LocationObtainer(locationManager) {
            @Override
            protected void onObtainStart() {
                showDialog(R.id.obtaining_location_dialog);
            };

            @Override
            protected void onObtainFinish(Location location) {
                // when current location is defined start Google Places search
                // service.
                new PlaceFinderTask(GplacesActivity.this).execute(location);
                removeDialog(R.id.obtaining_location_dialog);
            };
        };
        obtainer.start();
    }

    // method populates listview with new data.
    private void fillPlacesView(List<Place> places) {
        // sort list of places (default sort order by raiting desc).
        Collections.sort(places);
        placesList.clear();
        placesList.addAll(places);
        placesAdapter.notifyDataSetChanged();
    }

    // method queries particular contact data from addressbook
    private void sendSms(Uri contactData) {
        // validate if Place item was picked before sendSms method called.
        if (pickedPosition >= 0) {
            // query for contact data.
            Cursor cursor = managedQuery(contactData, null, null, null, null);
            if (cursor.moveToFirst()) {
                String phoneNumber = cursor.getString(cursor
                        .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                String contactName = cursor.getString(cursor
                        .getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                // get picked Place.
                Place place = placesAdapter.getItem(pickedPosition);
                Intent smsIntent = new Intent(Intent.ACTION_VIEW);
                smsIntent.putExtra(EXTRA_SMS_ADDRESS, phoneNumber);
                String msgText = getString(R.string.sms_text, contactName, place.getName());
                smsIntent.putExtra(EXTRA_SMS_BODY, msgText);
                smsIntent.setType(SMS_CONTENT_TYPE);
                startActivity(smsIntent);
                // reset picked position.
                pickedPosition = -1;
            }
        }
    }

    /**
     * Custom dialog key listener that allows dialog dismissing only on Back
     * button.
     * 
     * @author Anton Novikov
     */
    private static class DismissableKeyListener implements DialogInterface.OnKeyListener {
        @Override
        public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
            switch (keyCode) {
                case KeyEvent.KEYCODE_BACK:
                    dialog.dismiss();
                    return true;
                case KeyEvent.KEYCODE_SEARCH:
                    // prevent Search button from dismissing dialog.
                    return true;
                default:
                    return false;
            }
        }
    }

    /**
     * Asynchronous task for searching GIS objects via Google Places API.
     * Current task accepts location as an input parameter to find near by.
     * 
     * @author Anton Novikov
     */
    private static class PlaceFinderTask extends DialogBasedAsyncTask<Location, Void, List<Place>> {

        /**
         * Creates an instance of PlaceFinderTask.
         * 
         * @param activity activity this task is attached to
         */
        public PlaceFinderTask(GplacesActivity activity) {
            super(R.id.finding_progress_dialog, activity);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        protected AsyncTaskResult<List<Place>> doInBackground(Location... params) {
            // validate input parameters
            if (params == null || params.length != 1) {
                throw new IllegalArgumentException("Task supports only one input parameter.");
            }

            // construc service URL with all necessary data (location, search
            // radius, GIS type and apiKey).
            Context ctx = activity.getApplicationContext();
            Location location = params[0];
            String searchLocation = location.getLatitude() + "," + location.getLongitude();
            SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx);
            String radius = preferences.getString(SettingsActivity.SEARCH_RADIUS_PREF,
                    ctx.getString(R.string.default_search_radius));
            String type = preferences.getString(SettingsActivity.PLACES_TYPE_PREF,
                    ctx.getString(R.string.default_place_type));
            String url = ctx.getString(R.string.gplaces_url, searchLocation, radius, type,
                    ctx.getString(R.string.gplaces_key));

            AsyncTaskResult<List<Place>> result;
            InputStream jsonStream = null;
            try {
                // ask service for JSON content.
                URL serviceUrl = new URL(url);
                URLConnection connection = serviceUrl.openConnection();
                connection.connect();
                jsonStream = connection.getInputStream();
                // parse JSON to suitable object represenation.
                List<Place> places = PlacesParser.parseData(jsonStream);
                result = new AsyncTaskResult<List<Place>>(places);
            } catch (IOException e) {
                Log.e(TAG, "Unable to obtain data. Check your network.", e);
                result = new AsyncTaskResult<List<Place>>(R.id.network_error_code);
            } catch (JSONException e) {
                result = new AsyncTaskResult<List<Place>>(R.id.parser_error_code);
                Log.e(TAG, "JSON format is not supported.", e);
            } catch (QuotaLimitException e) {
                Log.e(TAG, "Unable to close Places data stream.", e);
                result = new AsyncTaskResult<List<Place>>(R.id.quota_limit_error_code);
            } catch (Exception e) {
                Log.e(TAG, "Unknown error.", e);
                result = new AsyncTaskResult<List<Place>>(R.id.unknown_error_code);
            } finally {
                if (jsonStream != null) {
                    try {
                        jsonStream.close();
                    } catch (IOException e) {
                        Log.e(TAG, "Unable to close Places data stream.", e);
                    }
                }
            }
            return result;
        }

        @Override
        protected void onPostExecute(AsyncTaskResult<List<Place>> result) {
            // check if async task finished while activity is attached to.
            if (activity != null) {
                int errorCode = result.getErrorCode();
                if (errorCode == R.id.no_errors_code) {
                    List<Place> places = result.getResult();
                    if (places.isEmpty()) {
                        activity.showDialog(R.id.no_results_dialog);
                    } else {
                        // populate list with new data.
                        ((GplacesActivity) activity).fillPlacesView(places);
                    }
                } else {
                    // show error dialog with specified code.
                    activity.showDialog(errorCode);
                }
            }
            super.onPostExecute(result);
        }
    }

    /**
     * Implementation of list adapter used to represent Places in the list view.
     * 
     * @author Anton Novikov
     */
    private static class PlacesAdapter extends BaseAdapter {
        private final List<Place> places;

        private final LayoutInflater inflater;

        /**
         * Creates an instance of PlacesAdapter.
         * 
         * @param places list of Places being stored in the adapter
         * @param context current application context
         */
        public PlacesAdapter(List<Place> places, Context context) {
            this.places = places;
            this.inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int getCount() {
            return places.size();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Place getItem(int position) {
            return places.get(position);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public long getItemId(int position) {
            return position;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
            if (convertView == null) {
                convertView = inflater.inflate(R.layout.list_item, parent, false);
                holder = new ViewHolder();
                holder.name = (TextView) convertView.findViewById(R.id.name);
                holder.raiting = (TextView) convertView.findViewById(R.id.raiting);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            Place place = getItem(position);
            holder.name.setText(place.getName());
            holder.raiting.setText(Double.toString(place.getRaiting()));
            return convertView;
        }
    }

    /**
     * Simple view holder used to cache the state of list view item and increase
     * performance on item inflating.
     * 
     * @author Anton Novikov
     */
    private static class ViewHolder {
        private TextView name;

        private TextView raiting;
    }

    /**
     * Simple state holder used to cache non-configuration data on changing
     * device orientation or other configuration.
     * 
     * @author Anton Novikov
     */
    private static class StateHolder {
        private List<Place> placesList;

        private int pickedPosition;
    }
}
